Integración y manejo de transacciones con Hibernate
El framework de Spring agrega mejoras significativas a la capa de ORM de nuestra elección cuando creamos aplicaciones que hacen uso de acceso a datos. Podemos dejarle a Spring tanto del trabajo de integración como queramos, teniendo la tranquilidad de usar un API probada y optimizada. También nos permite configurarlo de distintas maneras y a distintos niveles haciéndolo muy flexible al momento de tener que modificar algún aspecto del manejo o implementación de persistencia y/o transacciones de nuestra aplicación.
Suficiente teoría. Comencemos con la parte práctica del tutorial.
*Nota: Estamos actualizando los tutoriales de Hibernate para hacer uso de la versión 4, por lo que por ahora sólo deben saber que necesitan los siguientes jars, de la versión 4.2.7 de Hibernate: antlr-2.7.7.jar, dom4j-1.6.1.jar, hibernate-commons-annotations-4.0.2.Final.jar, hibernate-core-4.2.7.SP1.jar, hibernate-jpa-2.0-api-1.0.1.Final.jar, javassist-3.18.1-GA.jar, jboss-logging-3.1.0.GA.jar, jboss-transaction-api_1.1_spec-1.0.1.Final.jar
Hacemos clic derecho sobre el nodo «Libraries» del proyecto, en el menú que aparece elegimos la opción «Add Library…»
En la ventana que se abre seleccionamos las bibliotecas «Hibernate4», «Spring4» y «MySQL JDBC Driver»:
Hasta el momento nuestras bibliotecas deben verse de la siguiente forma:
Adicionalmente debemos agregar los jars con las clases necesarias de Spring para el manejo de persistencia:
- spring-orm-4.1.3.RELEASE.jar
- spring-jdbc-4.1.3.RELEASE.jar
- spring-tx-4.1.3.RELEASE.jar
Y si queremos que Spring maneje nuestras transacciones debemos agregar además los siguientes jars:
- spring-aop-4.1.3.RELEASE.jar
- com.springsource.org.aopalliance-1.0.0.jar (que pueden descargar desde java2s).
Nuestras bibliotecas finalmente deben verse así:
Ahora que ya tenemos las librerías, el siguiente paso es crear la base de datos. Usaremos una base de datos en MySQL que se llamara «tutorial_hibernate». En esta base de datos pondremos sólo una tabla que será «CONTACTO». Para crear esta tabla usaremos el siguiente script:
CREATE TABLE contacto (ID bigint(20) NOT NULL AUTO_INCREMENT,NOMBRE varchar(255) DEFAULT NULL,telefono varchar(255) DEFAULT NULL,email varchar(255) DEFAULT NULL,PRIMARY KEY (ID)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
También crearemos una clase, cuyas instancias serán guardadas en la tabla anterior. Para esto, primero crearemos un nuevo paquete llamado «entidades», y en este paquete creamos una nueva clase llamada «Contacto», que debe implementar la interface «java.io.Serializable»:
public class Contacto implements Serializable{}
Contacto será una clase muy sencilla que nos servirá para transportar los datos (un TO [Transfer Object] o DTO [Data Transfer Object]) desde y hacia la base de datos, por lo que sólo tendrá unos cuantos atributos, que serán equivalentes a las columnas de la tabla «contacto» que tenemos en la base de datos (también colocaremos getters y setters de estos atributos):
public class Contacto implements Serializable {private long id;private String email;private String nombre;public long getId() {private String telefono;return id; }this.id = id;protected void setId(long id) { } public String getNombre() {this.nombre = nombre;return nombre; } public void setNombre(String nombre) { }public void setEmail(String email) {public String getEmail() { return email; } this.email = email; }public void setTelefono(String telefono) {public String getTelefono() { return telefono; } this.telefono = telefono; }}
En el código anterior, seguramente notaron que el método «setId» está declarado como «protected» y no como «public». Esto ocurre porque en este caso no queremos que el usuario pueda establecer el valor del identificador del objeto, queremos que sea la base de datos quien genere este valor y Hibernate quien lo coloque en el objeto. Haciendo el método protegido nos aseguramos de que sólo un objeto que extienda de «Contacto» pueda establecer el valor (la cual, por cierto, es la forma de trabajar de Hibernate).
En la clase «Contacto» pondremos además un par de constructores, uno vacío y otro que reciba todos los atributos (excepto el id). El primer constructor es un requisito de Hibernate y también puede ser «protected» por las mismas razones que expusimos hace un momento, y el segundo es para facilitarnos a nosotros la creación de los objetos.
protected Contacto() {}public Contacto(String nombre, String email, String telefono) {this.nombre = nombre; this.email = email;}this.telefono = telefono;
La clase «Contacto» completa queda de la siguiente forma:
public class Contacto implements Serializable {private long id;private String email;private String nombre;protected Contacto() {private String telefono;}public Contacto(String nombre, String email, String telefono) {this.nombre = nombre; this.email = email; this.telefono = telefono; }this.id = id;public long getId() { return id; } protected void setId(long id) { }this.nombre = nombre;public String getNombre() { return nombre; } public void setNombre(String nombre) { }this.email = email;public String getEmail() { return email; } public void setEmail(String email) { } public String getTelefono() {}return telefono; } public void setTelefono(String telefono) { this.telefono = telefono;}
public interface ContactosDAO {void actualizaContacto(Contacto contacto) throws HibernateException;void eliminaContacto(Contacto contacto) throws HibernateException;Contacto obtenContacto(long idContacto) throws HibernateException;long guardaContacto(Contacto contacto) throws HibernateException; List<Contacto> obtenListaContactos() throws HibernateException;}
El siguiente paso es crear una implementación de la interface anterior, que contenga la lógica para conectarse a la base de datos. Como en nuestro caso la implementación será realizada con Hibernate, nos basaremos en lo que aprendimos en la serie de tutoriales deHibernate. Recordemos que para poder hacer operaciones sobre la base de datos lo primero que necesitamos es obtener una instancia de la clase «org.hibernate.SessionFactory». Obtener esta instancia es un trabajo «pesado», esto quiere decir que se necesita algo de tiempo para hacerlo y que consume una cantidad considerable de memoria, por lo tanto la recomendación es que esta instancia sea creada una sola vez, y que sea al inicio de la aplicación.
En los tutoriales de Hibernate lográbamos esto con una clase de utilidad llamada «HibernateUtil», o extendiendo de una clase base «AbstracHibernateDAO», y creábamos la instancia de «SessionFactory» en un bloque de inicialización estática dentro de e esta clase.
En esta ocasión Spring se encargará de crear esta instancia, lo único que tendremos que hacer es configurar algunos datos para la creación de este «SessionFactory», en el archivo de configuración de Spring y este se encargará del resto. Además haremos uso de la inyección de dependencias para poder acceder a la instancia de «SessionFactory» creada por Spring. Veremos cómo hacer esto un poco más adelante en el tutorial.
Creamos una nueva clase, en el paquete «persistencia», llamada «ContactosDAOHibernateImpl». Esta clase implementará la interface «ContactosDAO»:
public class ContactosDAOHibernateImpl implements ContactosDAO{}
public class ContactosDAOHibernateImpl implements ContactosDAO {private Session sesion;private SessionFactory sessionFactory;private Transaction transaction;}
También colocaremos un setter para el «SessionFactory». ¿Por qué? Porque recordemos que en este caso será Spring quien se encargará de crear este objeto, y lo pasará a nuestra clase a través de ese setter:
public class ContactosDAOHibernateImpl implements ContactosDAO {private Session sesion;private SessionFactory sessionFactory;private Transaction transaction;public void setSessionFactory(SessionFactory sessionFactory) {this.sessionFactory = sessionFactory; }}
private void iniciaOperacion() throws HibernateException {sesion = sessionFactory.getCurrentSession();}transaction = sesion.beginTransaction();
private void manejaExcepcion(HibernateException he) throws HibernateException {transaction.rollback();}
public class ContactosDAOHibernateImpl implements ContactosDAO {private Session sesion;private SessionFactory sessionFactory;private Transaction transaction;public void setSessionFactory(SessionFactory sessionFactory) {this.sessionFactory = sessionFactory; } @Overridetry {public long guardaContacto(Contacto contacto) throws HibernateException { long id = 0; iniciaOperacion();manejaExcepcion(he);id = (Long) sesion.save(contacto); transaction.commit(); } catch (HibernateException he) { throw he; } finally {iniciaOperacion();sesion.close(); } return id; } @Override public void actualizaContacto(Contacto contacto) throws HibernateException { try {sesion.close();sesion.update(contacto); transaction.commit(); } catch (HibernateException he) { manejaExcepcion(he); throw he; } finally { } }} catch (HibernateException he) {@Override public void eliminaContacto(Contacto contacto) throws HibernateException { try { iniciaOperacion(); sesion.delete(contacto); transaction.commit();Contacto contacto = null;manejaExcepcion(he); throw he; } finally { sesion.close(); } } @Override public Contacto obtenContacto(long idContacto) throws HibernateException { try {public List<Contacto> obtenListaContactos() throws HibernateException {iniciaOperacion(); contacto = (Contacto) sesion.get(Contacto.class, idContacto); } finally { sesion.close(); } return contacto; } @Override List<Contacto> listaContactos = null;private void iniciaOperacion() throws HibernateException {try { iniciaOperacion(); listaContactos = sesion.createQuery("from Contacto").list(); } finally { sesion.close(); } return listaContactos; } sesion = sessionFactory.getCurrentSession();}transaction = sesion.beginTransaction(); } private void manejaExcepcion(HibernateException he) throws HibernateException { transaction.rollback();}
Hacemos clic en el botón «Finish», con lo que deberemos de ver nuestro archivo de configuración, el cual tiene más o menos el siguiente contenido:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:aop="http://www.springframework.org/schema/aop"xmlns:context="http://www.springframework.org/schema/context"xmlns:tx="http://www.springframework.org/schema/tx"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd</beans>
Lo primero que haremos es crear el dataSource, o la conexión hacia la base de datos.Spring ofrece varias opciones para configurar esta fuente de datos, entre las que se incluyen:Data sources a través de JNDIData sources a través de drivers JDBCData sources que usan un pool de conexionesObviamente hay algunas diferencias en las características de cada uno de estos tipos de data sources.Si queremos simular un ambiente real de producción, o los parámetros de nuestra conexión cambiarán dependiendo del ambiente en el que estemos (por ejemplo: desarrollo, pruebas, producción) o estamos desarrollando un proyecto que será instalado en múltiples clientes, o simplemente no tenemos los datos de la conexión, pero nos será proporcionada a través de JNDI (por ejemplo si instalamos nuestra aplicación en un servidor de aplicaciones), lo más recomendable es usar un data source a través de JNDI.El elemento "<jee:jndi-lookup>" permite recuperar cualquier objeto, incluyendo data sources, de JNDI y ponerlo disponible como un bean de Spring. El atributo "jndi-name", de este elemento, se usa para especificar el nombre del recurso JNDI. Finalmente, si la aplicación se ejecuta en un servidor de aplicaciones, podemos establecer la propiedad "resource-ref" a "true", con lo que el nombre del recurso será precedido con "comp/env/".Por ejemplo, si tenemos un recurso de tipo data source, cuyo nombre es "conexionTutorial", entonces haremos referencia a este recurso de la siguiente forma:<jee:jndi-lookup id="dataSource" jndi-name="/jdbc/conexionTutorial" resource-ref="true" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"><property name="driverClassName" value="com.mysql.jdbc.Driver" /><property name="url" value="jdbc:mysql://localhost/tutorial_hibernate" /><property name="username" value="user" /><property name="password" value="password" /><property name="initialSize" value="5" /></bean><property name="maxActive" value="10" />
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"><property name="driverClassName" value="com.mysql.jdbc.Driver" /><property name="username" value="user" /><property name="url" value="jdbc:mysql://localhost/tutorial_hibernate" /><property name="password" value="password" /></bean>
Nosotros elegiremos trabajar con este último data source, por lo que hasta ahora nuestro archivo de configuración de Spring (omitiendo los namespaces) se ve de la siguiente forma:
<beans><bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"><property name="driverClassName" value="com.mysql.jdbc.Driver" /><property name="username" value="user" /><property name="url" value="jdbc:mysql://localhost/tutorial_hibernate" /><property name="password" value="password" /> </bean></beans>
El siguiente paso es crear el SessionFactory que, como recordaremos, nos permitirá crear objetos «Session» con los que interactuaremos con la base de datos. Para crear este objeto nuevamente declararemos un bean de Spring. La clase que usemos dependerá del framework ORM que estemos usando. Como en nuestro caso usamos Hibernate 4, la clase que tenemos que usar es «org.springframework.orm.hibernate4.LocalSessionFactoryBean»:
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"></bean>
Dentro de este bean podemos configuraremos algunos parámetros del framework ORM que estemos usando, en este caso Hibernate. Aquí pondremos parámetros que normalmente colocamos en el archivo de configuración de Hibernate («hibernate.cfg.xml»), junto con algunos otros datos.
Alguna de esta información dependerá si estamos trabajando con archivos de configuración en XML, o si estamos trabajando con anotaciones. Veremos la configuración común para ambas opciones y un poco más adelante veremos los elementos particulares para cada forma de trabajo.
Lo primero que debemos hacer es indicar el data source que se usará para conectarse a la base de datos, para ello usamos la propiedad «dataSource» y hacemos referencia al bean «dataSource» que creamos hace un momento:
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"><property name="dataSource"><ref bean="dataSource"/></bean></property>
Lo siguiente es configurar las propiedades de Hibernate que no tienen que ver con la conexión a la base de datos, como por ejemplo pool de conexiones, caché, el dialecto que se usará, los archivos de mapeo o clases anotadas que representan las entidades, etc. De hecho, si ya contamos con un archivo "hibernate.cfg.xml" o simplemente queremos apartar esta información del archivo de configuración de Spring (por ejemplo si queremos que esta configuración pueda ser modificada por otra persona o no queremos llenar este archivo con mucha información), este bean nos permite hacer referencia a un archivo de configuración de Hibernate del cual leerá estos parámetros. Para esto usamos la propiedad "configLocation", al cual le pasamos una lista de ubicaciones de los archivos que utilizará, en este caso sólo tendremos uno, por lo que la propiedad queda de esta forma:<property name="configLocation"><value>classpath:hibernate.cfg.xml</value></property>
Creamos este archivo, en la raíz del nodo «Source Packages» del proyecto, como un documento XML:
En este archivo pondremos la información del dialecto de la base de datos, indicaremos que no queremos que se muestre el SQLgenerado por Hibernate, y que la base de datos debe eliminarse ni actualizarse cada vez que hagamos una conexión. El archivo hasta ahora tiene el siguiente contenido:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE hibernate-configuration PUBLIC"-//Hibernate/Hibernate Configuration DTD 3.0//EN""http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"><hibernate-configuration><session-factory><property name="dialect">org.hibernate.dialect.MySQL5Dialect</property><!-- Dialecto de la base de datos --><property name="show_sql">false</property><!-- Otras propiedades importantes --></hibernate-configuration><property name="hbm2ddl.auto">none</property></session-factory>
Lo siguiente dependerá de si estamos usando archivos de mapeo en XML o anotaciones. En el primer caso debemos indicar dónde se encuentra el archivo de mapeo de la clase «Contacto» (archivo que no hemos creado todavía). Supongamos que lo hemos puesto en un directorio especial para los mapeos, debajo del paquete de las entidades, entonces tendremos que usar el atributo «resource» del elemento «mapping» de la siguiente forma:
<mapping resource="com/javatutoriales/spring/integration/hibernate/entidades/mapeos/Contacto.hbm.xml"/>
Si por el contrario, estuviéramos usando anotaciones, deberemos usar el atributo «class» del elemento «mapping», haciendo referencia a la clase «Contacto», de esta forma:
<mapping class="com.javatutoriales.spring.integration.hibernate.entidades.Contacto"/>
El archivo de configuración final de Hibernate depende entonces de si usamos anotaciones o archivos de mapeo. El archivo de configuración de Spring hasta ahora se ve de la siguiente forma:
<?xml version="1.0" encoding="UTF-8"?><beans><bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"><property name="driverClassName" value="com.mysql.jdbc.Driver" /><property name="username" value="user" /><property name="url" value="jdbc:mysql://localhost/tutorial_hibernate" /> <property name="password" value="password" /> </bean><ref bean="dataSource"/><bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource"> </property> <property name="configLocation"><mapping resource="com/javatutoriales/spring/integration/hibernate/entidades/mapeos/Contacto.hbm.xml"/><value>classpath:hibernate.cfg.xml</value> </property> <!-- mapping class="com.javatutoriales.spring.integration.hibernate.entidades.Contacto"/--> </bean></beans>
Ahora, ¿qué pasa si no queremos poner esta información en el archivo de Hibernate, sino directamente en el archivo de Spring? Para eso podemos usar la propiedad «hibernateProperties» del bean «sessionFactory». En esta propiedad colocaremos un conjunto de, valga la redundancia, propiedades que indican los valores de las (si, es la tercera vez) propiedades de Hibernate. Para reproducir lo que teníamos en el archivo de configuración de Hibernate, debemos colocar las propiedades de esta forma:
<property name="hibernateProperties"><props><prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop><prop key="hibernate.show_sql">true</prop></props></property>
Los más observadores se habrán percatado de que aún no hemos colocado la información de en dónde se encuentran las entidades (o los archivos de mapeos de las mismas) que serán manejadas por Hibernate. Para esto tenemos cuatro maneras, las primeras dos para cuando trabajamos con archivos de mapeo en XML.
La primera de las formas consiste en indicar, a través de la propiedad «mappingResources» del bean «sessionFactory», donde se encuentra cada uno de los archivos de mapeo de nuestras entidades, a través de una lista de valores; de la siguiente forma:
<property name="mappingResources"><list><value>com/javatutoriales/spring/integracion/mapeos/Contacto.hbm.xml</value></list></property>
Esto está bien si tenemos pocas entidades pero en aplicaciones de tamaño mediano a grande este no será el caso, y no sólo tener colocar todos los archivos de mapeo puede ser tardado, sino que el tener que estar quitando los de las entidades que quitamos del diseño de nuestro sistema, o tener que recordar agregar los nuevos, puede ser una labor propensa a errores. Debido a esto les recomiendo el uso de otra propiedad del bean «sessionFactory», llamada «mappingDirectoryLocations» (la cual es la segunda forma de indicar dónde están nuestras entidades). En esta propiedad lo único que debemos hacer es indicar los lugares en los que se encuentran los archivos de mapeo, y digo lugares por si tenemos nuestros archivos de mapeo esparcidos por varios lugares de nuestra aplicación (lo cual no debería de pasar), la propiedad de configura de la siguiente forma:
<property name="mappingDirectoryLocations"><list><value>classpath:com/javatutoriales/spring/integracion/mapeos/</value></list></property>
Y la configuración completa de nuestro bean «sessionFactory» se ve de la siguiente forma:
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"><property name="dataSource"><ref bean="dataSource"/><prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop></property><property name="hibernateProperties"><props><property name="mappingDirectoryLocations"><prop key="hibernate.show_sql">true</prop> </props> </property> <list></bean><value>classpath:com/javatutoriales/spring/integracion/mapeos/</value> </list></property>
La tercera forma de configurar la ubicación de nuestras entidades manejadas por Hibernate es para cuando anotamos estas entidades. Podemos usar la propiedad «annotatedClasses» para indicar cada una de las clases que están anotadas como entidades, de la siguiente manera:
<property name="annotatedClasses"><list><value>com.javatutoriales.spring.integracion.entidades.Contacto</value></list></property>
Aquí tendremos el mismo problema que cuando indicamos los archivos de mapeo uno por uno, por lo que les recomiendo que usen la cuarta forma, que es a través de la propiedad «packagesToScan» del bean «sessionFactory», en la cual indicamos la lista de los paquetes en los que se encuentran nuestras entidades, de la siguiente manera:
<property name="packagesToScan"><list><value>com.javatutoriales.spring.integracion.entidades</value></list></property>
Por lo que la configuración del bean «sessionFactory» queda de la siguiente forma si trabajamos con anotaciones:
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"><property name="dataSource"><ref bean="dataSource"/><prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop></property><property name="hibernateProperties"><props><prop key="hibernate.hbm2ddl.auto">none</prop><prop key="hibernate.show_sql">true</prop> </props> </property><value>com.javatutoriales.spring.integration.hibernate.entidades</value><property name="packagesToScan"> <list> </list> </property></bean>
Y así si trabajamos con archivos de mapeo:
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"><property name="dataSource"><ref bean="dataSource"/><prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop></property><property name="hibernateProperties"><props><prop key="hibernate.hbm2ddl.auto">none</prop><prop key="hibernate.show_sql">true</prop> </props> </property><value>classpath:com/javatutoriales/spring/integration/hibernate/entidades/mapeos</value><property name="mappingDirectoryLocations"> <list> </list> </property></bean>
Ahora que tenemos la estructura básica para los ejemplos dividiremos este tutorial en dos partes. Recordemos que cada uno de estos frameworks tiene dos formas de ser configurados: con archivos de configuración en XML y con Anotaciones. En la primer parte del tutorial veremos sólo la parte de trabajo con archivos de configuración en XML, y en la segunda parte veremos cómo trabajar con anotaciones. Pero recuerden que pueden usar ambas formas de trabajo mezcladas a su conveniencia.








